<!DOCTYPE html>
<meta charset=utf-8>
<title>Test chrome-only MutationObserver animation notifications</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script src="../testcommon.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<style>
@keyframes anim {
  to { transform: translate(100px); }
}
@keyframes anotherAnim {
  to { transform: translate(0px); }
}
#target {
  width: 100px;
  height: 100px;
  background-color: yellow;
  line-height: 16px;
}
</style>
<div id=container><div id=target></div></div>
<script>
var div = document.getElementById("target");
var gRecords = [];
var gObserver = new MutationObserver(function(newRecords) {
  gRecords.push(...newRecords);
});

// Asynchronous testing framework based on layout/style/test/animation_utils.js.

var gTests = [];
var gCurrentTestName;

function addAsyncAnimTest(aName, aOptions, aTestGenerator) {
  aTestGenerator.testName = aName;
  aTestGenerator.options = aOptions || {};
  gTests.push(aTestGenerator);
}

function runAsyncTest(aTestGenerator) {
  return waitForFrame().then(function() {
    var generator;

    function step(arg) {
      var next;
      try {
        next = generator.next(arg);
      } catch (e) {
        return Promise.reject(e);
      }
      if (next.done) {
        return Promise.resolve(next.value);
      } else {
        return Promise.resolve(next.value).then(step);
      }
    }

    var subtree = aTestGenerator.options.subtree;

    gCurrentTestName = aTestGenerator.testName;
    if (subtree) {
      gCurrentTestName += ":subtree";
    }

    gRecords = [];
    gObserver.disconnect();
    gObserver.observe(aTestGenerator.options.observe,
                      { animations: true, subtree: subtree});

    generator = aTestGenerator();
    return step();
  });
};

function runAllAsyncTests() {
  return gTests.reduce(function(sequence, test) {
    return sequence.then(() => runAsyncTest(test));
  }, Promise.resolve());
}

// Wrap is and ok with versions that prepend the current sub-test name
// to the assertion description.
var old_is = is, old_ok = ok;
is = function(a, b, message) {
  if (gCurrentTestName && message) {
    message = `[${gCurrentTestName}] ${message}`;
  }
  old_is(a, b, message);
}
ok = function(a, message) {
  if (gCurrentTestName && message) {
    message = `[${gCurrentTestName}] ${message}`;
  }
  old_ok(a, message);
}

// Adds an event listener and returns a Promise that is resolved when the
// event listener is called.
function await_event(aElement, aEventName) {
  return new Promise(function(aResolve) {
    function listener(aEvent) {
      aElement.removeEventListener(aEventName, listener);
      aResolve();
    }
    aElement.addEventListener(aEventName, listener, false);
  });
}

function assert_record_list(actual, expected, desc, index, listName) {
  is(actual.length, expected.length, `${desc} - record[${index}].${listName} length`);
  if (actual.length != expected.length) {
    return;
  }
  for (var i = 0; i < actual.length; i++) {
    ok(actual.indexOf(expected[i]) != -1,
       `${desc} - record[${index}].${listName} contains expected Animation`);
  }
}

function assert_records(expected, desc) {
  var records = gRecords;
  gRecords = [];
  is(records.length, expected.length, `${desc} - number of records`);
  if (records.length != expected.length) {
    return;
  }
  for (var i = 0; i < records.length; i++) {
    assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations");
    assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations");
    assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations");
  }
}

// -- Tests ------------------------------------------------------------------

// We run all tests first targeting the div and observing the div, then again
// targeting the div and observing its parent while using the subtree:true
// MutationObserver option.

[
  { observe: div,            target: div, subtree: false },
  { observe: div.parentNode, target: div, subtree: true  },
].forEach(function(aOptions) {

  var e = aOptions.target;

  // Test that starting a single transition that completes normally
  // dispatches an added notification and then a removed notification.
  addAsyncAnimTest("single_transition", aOptions, function*() {
    // Start a transition.
    e.style = "transition: background-color 100s; background-color: lime;";

    // Register for the end of the transition.
    var transitionEnd = await_event(e, "transitionend");

    // The transition should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after transition start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after transition start");

    // Advance until near the end of the transition, then wait for it to finish.
    animations[0].currentTime = 99900;
    yield transitionEnd;

    // After the transition has finished, the Animation should disappear.
    is(e.getAnimations().length, 0,
       "getAnimations().length after transition end");

    // Wait for the change MutationRecord for seeking the Animation to be
    // delivered, followed by the the removal MutationRecord.
    yield waitForFrame();
    assert_records([{ added: [], changed: animations, removed: [] },
                    { added: [], changed: [], removed: animations }],
                   "records after transition end");

    e.style = "";
  });

  // Test that starting a single transition that is cancelled by resetting
  // the transition-property property dispatches an added notification and
  // then a removed notification.
  addAsyncAnimTest("single_transition_cancelled_property", aOptions, function*() {
    // Start a long transition.
    e.style = "transition: background-color 100s; background-color: lime;";

    // The transition should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after transition start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after transition start");

    // Cancel the transition by setting transition-property.
    e.style.transitionProperty = "none";

    // Wait for the single MutationRecord for the Animation removal to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                   "records after transition end");

    e.style = "";
  });

  // Test that starting a single transition that is cancelled by setting
  // style to the currently animated value dispatches an added
  // notification and then a removed notification.
  addAsyncAnimTest("single_transition_cancelled_value", aOptions, function*() {
    // Start a long transition with a predictable value.
    e.style = "transition: background-color 100s steps(2, end) -51s; background-color: lime;";

    // The transition should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after transition start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after transition start");

    // Cancel the transition by setting the current animation value.
    var value = "rgb(128, 255, 0)";
    is(getComputedStyle(e).backgroundColor, value, "half-way transition value");
    e.style.backgroundColor = value;

    // Wait for the single MutationRecord for the Animation removal to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                   "records after transition end");

    e.style = "";
  });

  // Test that starting a single transition that is cancelled by setting
  // style to a non-interpolable value dispatches an added notification
  // and then a removed notification.
  addAsyncAnimTest("single_transition_cancelled_noninterpolable", aOptions, function*() {
    // Start a long transition.
    e.style = "transition: line-height 100s; line-height: 100px;";

    // The transition should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after transition start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after transition start");

    // Cancel the transition by setting line-height to a non-interpolable value.
    e.style.lineHeight = "normal";

    // Wait for the single MutationRecord for the Animation removal to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                   "records after transition end");

    e.style = "";
  });

  // Test that starting a single transition and then reversing it
  // dispatches an added notification, then a simultaneous removed and
  // added notification, then a removed notification once finished.
  addAsyncAnimTest("single_transition_reversed", aOptions, function*() {
    // Start a long transition.
    e.style = "transition: background-color 100s step-start; background-color: lime;";

    // The transition should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after transition start");

    var firstAnimation = animations[0];

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [firstAnimation], changed: [], removed: [] }],
                   "records after transition start");

    // Wait for the Animation to be playing, then seek well into
    // the transition.
    yield firstAnimation.ready;
    firstAnimation.currentTime = 50 * MS_PER_SEC;

    // Reverse the transition by setting the background-color back to its
    // original value.
    e.style.backgroundColor = "yellow";

    // The reversal should cause the creation of a new Animation.
    animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after transition reversal");

    var secondAnimation = animations[0];

    ok(firstAnimation != secondAnimation,
       "second Animation should be different from the first");

    // Wait for the change Mutation record from seeking the first animation
    // to be delivered, followed by a subsequent MutationRecord for the removal
    // of the original Animation and the addition of the new Animation.
    yield waitForFrame();
    assert_records([{ added: [], changed: [firstAnimation], removed: [] },
                    { added: [secondAnimation], changed: [],
                      removed: [firstAnimation] }],
                   "records after transition reversal");

    // Cancel the transition.
    e.style.transitionProperty = "none";

    // Wait for the single MutationRecord for the Animation removal to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: [secondAnimation] }],
                   "records after transition end");

    e.style = "";
  });

  // Test that multiple transitions starting and ending on an element
  // at the same time get batched up into a single MutationRecord.
  addAsyncAnimTest("multiple_transitions", aOptions, function*() {
    // Start three long transitions.
    e.style = "transition-duration: 100s; " +
              "transition-property: color, background-color, line-height; " +
              "color: blue; background-color: lime; line-height: 24px;";

    // The transitions should cause the creation of three Animations.
    var animations = e.getAnimations();
    is(animations.length, 3, "getAnimations().length after transition starts");

    // Wait for the single MutationRecord for the Animation additions to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after transition starts");

    // Wait for the Animations to get going.
    yield animations[0].ready;
    is(animations.filter(p => p.playState == "running").length, 3,
       "number of running Animations");

    // Seek well into each animation.
    animations.forEach(p => p.currentTime = 50 * MS_PER_SEC);

    // Prepare the set of expected change MutationRecords, one for each
    // animation that was seeked.
    var seekRecords = animations.map(
      p => ({ added: [], changed: [p], removed: [] })
    );

    // Cancel one of the transitions by setting transition-property.
    e.style.transitionProperty = "background-color, line-height";

    var colorAnimation  = animations.filter(p => p.playState != "running");
    var otherAnimations = animations.filter(p => p.playState == "running");

    is(colorAnimation.length, 1,
       "number of non-running Animations after cancelling one");
    is(otherAnimations.length, 2,
       "number of running Animations after cancelling one");

    // Wait for the MutationRecords to be delivered: one for each animation
    // that was seeked, followed by one for the removal of the color animation.
    yield waitForFrame();
    assert_records(seekRecords.concat(
                    { added: [], changed: [], removed: colorAnimation }),
                   "records after color transition end");

    // Cancel the remaining transitions.
    e.style.transitionProperty = "none";

    // Wait for the MutationRecord for the other two Animation
    // removals to be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: otherAnimations }],
                   "records after other transition ends");

    e.style = "";
  });

  // Test that starting a single animation that completes normally
  // dispatches an added notification and then a removed notification.
  addAsyncAnimTest("single_animation", aOptions, function*() {
    // Start an animation.
    e.style = "animation: anim 100s;";

    // Register for the end of the animation.
    var animationEnd = await_event(e, "animationend");

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after animation start");

    // Advance until near the end of the animation, then wait for it to finish.
    animations[0].currentTime = 99900;
    yield animationEnd;

    // After the animation has finished, the Animation should disappear.
    is(e.getAnimations().length, 0,
       "getAnimations().length after animation end");

    // Wait for the change MutationRecord from seeking the Animation to
    // be delivered, followed by a further MutationRecord for the Animation
    // removal.
    yield waitForFrame();
    assert_records([{ added: [], changed: animations, removed: [] },
                    { added: [], changed: [], removed: animations }],
                   "records after animation end");

    e.style = "";
  });

  // Test that starting a single animation that is cancelled by resetting
  // the animation-name property dispatches an added notification and
  // then a removed notification.
  addAsyncAnimTest("single_animation_cancelled_name", aOptions, function*() {
    // Start a long animation.
    e.style = "animation: anim 100s;";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after animation start");

    // Cancel the animation by setting animation-name.
    e.style.animationName = "none";

    // Wait for the single MutationRecord for the Animation removal to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                   "records after animation end");

    e.style = "";
  });

  // Test that starting a single animation that is cancelled by updating
  // the animation-duration property dispatches an added notification and
  // then a removed notification.
  addAsyncAnimTest("single_animation_cancelled_duration", aOptions, function*() {
    // Start a long animation.
    e.style = "animation: anim 100s;";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after animation start");

    // Advance the animation by a second.
    animations[0].currentTime += 1 * MS_PER_SEC;

    // Cancel the animation by setting animation-duration to a value less
    // than a second.
    e.style.animationDuration = "0.1s";

    // Wait for the change MutationRecord from seeking the Animation to
    // be delivered, followed by a further MutationRecord for the Animation
    // removal.
    yield waitForFrame();
    assert_records([{ added: [], changed: animations, removed: [] },
                    { added: [], changed: [], removed: animations }],
                   "records after animation end");

    e.style = "";
  });

  // Test that starting a single animation that is cancelled by updating
  // the animation-delay property dispatches an added notification and
  // then a removed notification.
  addAsyncAnimTest("single_animation_cancelled_delay", aOptions, function*() {
    // Start a long animation.
    e.style = "animation: anim 100s;";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after animation start");

    // Cancel the animation by setting animation-delay.
    e.style.animationDelay = "-200s";

    // Wait for the single MutationRecord for the Animation removal to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                   "records after animation end");

    e.style = "";
  });

  // Test that starting a single animation that is cancelled by updating
  // the animation-fill-mode property dispatches an added notification and
  // then a removed notification.
  addAsyncAnimTest("single_animation_cancelled_fill", aOptions, function*() {
    // Start a short, filled animation.
    e.style = "animation: anim 100s forwards;";

    // Register for the end of the animation.
    var animationEnd = await_event(e, "animationend");

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after animation start");

    // Advance until near the end of the animation, then wait for it to finish.
    animations[0].currentTime = 99900;
    yield animationEnd;

    // The only MutationRecord at this point should be the change from
    // seeking the Animation.
    assert_records([{ added: [], changed: animations, removed: [] }],
                   "records after animation starts filling");

    // Cancel the animation by setting animation-fill-mode.
    e.style.animationFillMode = "none";

    // Wait for the single MutationRecord for the Animation removal to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                   "records after animation end");

    e.style = "";
  });

  // Test that starting a single animation that is cancelled by updating
  // the animation-iteration-count property dispatches an added notification
  // and then a removed notification.
  addAsyncAnimTest("single_animation_cancelled_iteration_count",
                   aOptions, function*() {
    // Start a short, repeated animation.
    e.style = "animation: anim 0.5s infinite;";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after animation start");

    // Advance the animation until we are past the first iteration.
    animations[0].currentTime += 1 * MS_PER_SEC;

    // The only MutationRecord at this point should be the change from
    // seeking the Animation.
    yield waitForFrame();
    assert_records([{ added: [], changed: animations, removed: [] }],
                   "records after seeking animations");

    // Cancel the animation by setting animation-iteration-count.
    e.style.animationIterationCount = "1";

    // Wait for the single MutationRecord for the Animation removal to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                   "records after animation end");

    e.style = "";
  });

  // Test that updating an animation property dispatches a changed notification.
  [
    { name: "duration",  prop: "animationDuration",       val: "200s"    },
    { name: "timing",    prop: "animationTimingFunction", val: "linear"  },
    { name: "iteration", prop: "animationIterationCount", val: "2"       },
    { name: "direction", prop: "animationDirection",      val: "reverse" },
    { name: "state",     prop: "animationPlayState",      val: "paused"  },
    { name: "delay",     prop: "animationDelay",          val: "-1s"     },
    { name: "fill",      prop: "animationFillMode",       val: "both"    },
  ].forEach(function(aChangeTest) {
    addAsyncAnimTest(`single_animation_change_${aChangeTest.name}`, aOptions, function*() {
      // Start a long animation.
      e.style = "animation: anim 100s;";

      // The animation should cause the creation of a single Animation.
      var animations = e.getAnimations();
      is(animations.length, 1, "getAnimations().length after animation start");

      // Wait for the single MutationRecord for the Animation addition to
      // be delivered.
      yield waitForFrame();
      assert_records([{ added: animations, changed: [], removed: [] }],
                     "records after animation start");

      // Change a property of the animation such that it keeps running.
      e.style[aChangeTest.prop] = aChangeTest.val;

      // Wait for the single MutationRecord for the Animation change to
      // be delivered.
      yield waitForFrame();
      assert_records([{ added: [], changed: animations, removed: [] }],
                     "records after animation change");

      // Cancel the animation.
      e.style.animationName = "none";

      // Wait for the addition, change and removal MutationRecords to be delivered.
      yield waitForFrame();
      assert_records([{ added: [], changed: [], removed: animations }],
                      "records after animation end");

      e.style = "";
    });
  });

  // Test that calling finish() on a paused (but otherwise finished) animation
  // dispatches a changed notification.
  addAsyncAnimTest("finish_from_pause", aOptions, function*() {
    // Start a long animation
    e.style = "animation: anim 100s forwards";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                    "records after animation start");

    // Wait until the animation is playing.
    yield animations[0].ready;

    // Finish and pause.
    animations[0].finish();
    animations[0].pause();

    // Wait for the pause to complete.
    yield animations[0].ready;
    is(animations[0].playState, "paused",
       "playState after finishing and pausing");

    // We should have two MutationRecords for the Animation changes:
    // one for the finish, one for the pause.
    assert_records([{ added: [], changed: animations, removed: [] },
                    { added: [], changed: animations, removed: [] }],
                    "records after finish() and pause()");

    // Call finish() again.
    animations[0].finish();
    is(animations[0].playState, "finished",
       "playState after finishing from paused state");

    // Wait for the single MutationRecord for the Animation change to
    // be delivered. Even though the currentTime does not change, the
    // playState will change.
    yield waitForFrame();
    assert_records([{ added: [], changed: animations, removed: [] }],
                    "records after finish() and pause()");

    // Cancel the animation.
    e.style = "";

    // Wait for the single removal notification.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                    "records after animation end");
  });

  // Test that calling finish() on a pause-pending (but otherwise finished)
  // animation dispatches a changed notification.
  addAsyncAnimTest("finish_from_pause_pending", aOptions, function*() {
    // Start a long animation
    e.style = "animation: anim 100s forwards";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                    "records after animation start");

    // Wait until the animation is playing.
    yield animations[0].ready;

    // Finish and pause.
    animations[0].finish();
    animations[0].pause();
    is(animations[0].playState, "pending",
       "playState after finishing and calling pause()");

    // Call finish() again to abort the pause
    animations[0].finish();
    is(animations[0].playState, "finished",
       "playState after finishing and calling pause()");

    // Wait for three MutationRecords for the Animation changes to
    // be delivered: one for each finish(), pause(), finish() operation.
    yield waitForFrame();
    assert_records([{ added: [], changed: animations, removed: [] },
                    { added: [], changed: animations, removed: [] },
                    { added: [], changed: animations, removed: [] }],
                    "records after finish(), pause(), finish()");

    // Cancel the animation.
    e.style = "";

    // Wait for the single removal notification.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                    "records after animation end");
  });

  // Test that calling play() on a paused Animation dispatches a changed
  // notification.
  addAsyncAnimTest("play", aOptions, function*() {
    // Start a long, paused animation
    e.style = "animation: anim 100s paused";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                    "records after animation start");

    // Wait until the animation is ready
    yield animations[0].ready;

    // Play
    animations[0].play();

    // Wait for the single MutationRecord for the Animation change to
    // be delivered.
    yield animations[0].ready;
    assert_records([{ added: [], changed: animations, removed: [] }],
                    "records after play()");

    // Redundant play
    animations[0].play();

    // Wait to ensure no change is dispatched
    yield waitForFrame();
    assert_records([], "records after redundant play()");

    // Cancel the animation.
    e.style = "";

    // Wait for the single removal notification.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                    "records after animation end");
  });

  // Test that calling play() on a finished Animation that fills forwards
  // dispatches a changed notification.
  addAsyncAnimTest("play_filling_forwards", aOptions, function*() {
    // Start a long animation with a forwards fill
    e.style = "animation: anim 100s forwards";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                    "records after animation start");

    // Wait until the animation is ready
    yield animations[0].ready;

    // Seek to the end
    animations[0].finish();

    // Wait for the single MutationRecord for the Animation change to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: animations, removed: [] }],
                    "records after finish()");

    // Since we are filling forwards, calling play() should produce a
    // change record since the animation remains relevant.
    animations[0].play();

    // Wait for the single MutationRecord for the Animation change to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: animations, removed: [] }],
                    "records after play()");

    // Cancel the animation.
    e.style = "";

    // Wait for the single removal notification.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                    "records after animation end");
  });

  // Test that calling play() on a finished Animation that does *not* fill
  // forwards dispatches an addition notification.
  addAsyncAnimTest("play_after_finish", aOptions, function*() {
    // Start a long animation
    e.style = "animation: anim 100s";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                    "records after animation start");

    // Wait until the animation is ready
    yield animations[0].ready;

    // Seek to the end
    animations[0].finish();

    // Wait for the single MutationRecord for the Animation removal to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                    "records after finish()");

    // Since we are *not* filling forwards, calling play() is equivalent
    // to creating a new animation since it becomes relevant again.
    animations[0].play();

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                    "records after play()");

    // Cancel the animation.
    e.style = "";

    // Wait for the single removal notification.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                    "records after animation end");
  });

  // Test that calling pause() on an Animation dispatches a changed
  // notification.
  addAsyncAnimTest("pause", aOptions, function*() {
    // Start a long animation
    e.style = "animation: anim 100s";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                    "records after animation start");

    // Wait until the animation is ready
    yield animations[0].ready;

    // Pause
    animations[0].pause();

    // Wait for the single MutationRecord for the Animation change to
    // be delivered.
    yield animations[0].ready;
    assert_records([{ added: [], changed: animations, removed: [] }],
                    "records after pause()");

    // Redundant pause
    animations[0].pause();

    // Wait to ensure no change is dispatched
    yield animations[0].ready;
    assert_records([], "records after redundant pause()");

    // Cancel the animation.
    e.style = "";

    // Wait for the single removal notification.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                    "records after animation end");
  });

  // Test that calling pause() on an Animation that is pause-pending
  // does not dispatch an additional changed notification.
  addAsyncAnimTest("pause_while_pause_pending", aOptions, function*() {
    // Start a long animation
    e.style = "animation: anim 100s";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                    "records after animation start");

    // Wait until the animation is ready
    yield animations[0].ready;

    // Pause
    animations[0].pause();

    // We are now pause-pending, but pause again
    animations[0].pause();

    // We should only get a single change record
    yield animations[0].ready;
    assert_records([{ added: [], changed: animations, removed: [] }],
                    "records after pause()");

    // Cancel the animation.
    e.style = "";

    // Wait for the single removal notification.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                    "records after animation end");
  });

  // Test that calling play() on an Animation that is pause-pending
  // dispatches a changed notification.
  addAsyncAnimTest("aborted_pause", aOptions, function*() {
    // Start a long animation
    e.style = "animation: anim 100s";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                    "records after animation start");

    // Wait until the animation is ready
    yield animations[0].ready;

    // Pause
    animations[0].pause();

    // We are now pause-pending. If we play() now, we will abort the pause
    animations[0].play();

    // We should get two change records
    yield animations[0].ready;
    assert_records([{ added: [], changed: animations, removed: [] },
                    { added: [], changed: animations, removed: [] }],
                    "records after aborting a pause()");

    // Cancel the animation.
    e.style = "";

    // Wait for the single removal notification.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                    "records after animation end");
  });

  // Test that a non-cancelling change to an animation followed immediately by a
  // cancelling change will only send an animation removal notification.
  addAsyncAnimTest("coalesce_change_cancel", aOptions, function*() {
    // Start a long animation.
    e.style = "animation: anim 100s;";

    // The animation should cause the creation of a single Animation.
    var animations = e.getAnimations();
    is(animations.length, 1, "getAnimations().length after animation start");

    // Wait for the single MutationRecord for the Animation addition to
    // be delivered.
    yield waitForFrame();
    assert_records([{ added: animations, changed: [], removed: [] }],
                   "records after animation start");

    // Update the animation's delay such that it is still running.
    e.style.animationDelay = "-1s";

    // Then cancel the animation by updating its duration.
    e.style.animationDuration = "0.5s";

    // We should get a single removal notification.
    yield waitForFrame();
    assert_records([{ added: [], changed: [], removed: animations }],
                   "records after animation end");

    e.style = "";
  });

});

addAsyncAnimTest("tree_ordering", { observe: div, subtree: true }, function*() {
  // Add style for pseudo elements
  var extraStyle = document.createElement('style');
  document.head.appendChild(extraStyle);
  var sheet = extraStyle.sheet;
  var rules = { ".before::before": "animation: anim 100s;",
                ".after::after"  : "animation: anim 100s, anim 100s;" };
  for (var selector in rules) {
    sheet.insertRule(selector + '{' + rules[selector] + '}',
                     sheet.cssRules.length);
  }

  // Create a tree with two children:
  //
  //          div
  //       (::before)
  //       (::after)
  //        /     \
  //   childA      childB(::before)
  var childA = document.createElement("div");
  var childB = document.createElement("div");

  div.appendChild(childA);
  div.appendChild(childB);

  // Start an animation on each (using order: childB, div, childA)
  //
  // We include multiple animations on some nodes so that we can test batching
  // works as expected later in this test.
  childB.style = "animation: anim 100s";
  div.style    = "animation: anim 100s, anim 100s, anim 100s";
  childA.style = "animation: anim 100s, anim 100s";

  // Start animations targeting to pseudo element of div and childB.
  childB.classList.add("before");
  div.classList.add("after");
  div.classList.add("before");

  // Check all animations we have in this document
  var docAnims = document.getAnimations();
  is(docAnims.length, 10, "total animations");

  var divAnimations = div.getAnimations();
  var childAAnimations = childA.getAnimations();
  var childBAnimations = childB.getAnimations();
  var divBeforeAnimations =
    [ for (x of docAnims) if (x.effect.target.parentElement == div &&
                              x.effect.target.type == "::before") x ];
  var divAfterAnimations =
    [ for (x of docAnims) if (x.effect.target.parentElement == div &&
                              x.effect.target.type == "::after") x ];
  var childBPseudoAnimations =
    [ for (x of docAnims) if (x.effect.target.parentElement == childB) x ];

  // The order in which we get the corresponding records is currently
  // based on the order we visit these nodes when updating styles.
  //
  // That is because we don't do any document-level batching of animation
  // mutation records when we flush styles. We may introduce that in the
  // future but for now all we are interested in testing here is that the order
  // these records are dispatched is consistent between runs.
  //
  // We currently expect to get records in order div::after, childA, childB,
  // childB::before, div, div::before
  yield waitForFrame();
  assert_records([{ added: divAfterAnimations, changed: [], removed: [] },
                  { added: childAAnimations, changed: [], removed: [] },
                  { added: childBAnimations, changed: [], removed: [] },
                  { added: childBPseudoAnimations, changed: [], removed: [] },
                  { added: divAnimations, changed: [], removed: [] },
                  { added: divBeforeAnimations, changed: [], removed: [] }],
                 "records after simultaneous animation start");

  // The one case where we *do* currently perform document-level (or actually
  // timeline-level) batching is when animations are updated from a refresh
  // driver tick. In particular, this means that when animations finish
  // naturally the removed records should be dispatched according to the
  // position of the elements in the tree.

  // First, flatten the set of animations. we put the animations targeting to
  // pseudo elements last. (Actually, we don't care the order in the list.)
  var animations = [ ...divAnimations,
                     ...childAAnimations,
                     ...childBAnimations,
                     ...divBeforeAnimations,
                     ...divAfterAnimations,
                     ...childBPseudoAnimations ];

  // Fast-forward to *just* before the end of the animation.
  animations.forEach(animation => animation.currentTime = 99999);

  // Prepare the set of expected change MutationRecords, one for each
  // animation that was seeked.
  var seekRecords = animations.map(
    p => ({ added: [], changed: [p], removed: [] })
  );

  yield await_event(div, "animationend");

  // After the changed notifications, which will be dispatched in the order that
  // the animations were seeked, we should get removal MutationRecords in order
  // (div, div::before, div::after), childA, (childB, childB::before).
  // Note: The animations targeting to the pseudo element are appended after
  //       the animations of its parent element.
  divAnimations = [ ...divAnimations,
                    ...divBeforeAnimations,
                    ...divAfterAnimations ];
  childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ];
  assert_records(seekRecords.concat(
                   { added: [], changed: [], removed: divAnimations },
                   { added: [], changed: [], removed: childAAnimations },
                   { added: [], changed: [], removed: childBAnimations }),
                 "records after finishing");

  // Clean up
  div.classList.remove("before");
  div.classList.remove("after");
  div.style = "";
  childA.remove();
  childB.remove();
  extraStyle.remove();
});

// Run the tests.
SimpleTest.requestLongerTimeout(2);
SimpleTest.waitForExplicitFinish();

runAllAsyncTests().then(function() {
  SimpleTest.finish();
}, function(aError) {
  ok(false, "Something failed: " + aError);
});
</script>
